package com.seventeen.monster.redisson.core;

import com.seventeen.monster.redisson.annotation.DistributeLock;
import com.seventeen.monster.redisson.support.CustomExpressionParser;
import com.seventeen.monster.redisson.support.LockExpressionRoot;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.StandardReflectionParameterNameDiscoverer;
import org.springframework.util.ObjectUtils;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

/**
 * @author seventeen
 */
@Aspect
public class DistributeLockAop {
    private final static Logger log = LoggerFactory.getLogger(DistributeLockAop.class);

    private final RedissonClient redissonClient;

    public DistributeLockAop(RedissonClient redissonClient) {
        this.redissonClient = redissonClient;
    }

    @Pointcut("@annotation(com.seventeen.monster.redisson.annotation.DistributeLock)")
    public void pointcut() {
    }

    @Around("pointcut()")
    public Object process(ProceedingJoinPoint joinPoint) throws Throwable {
        Object target = joinPoint.getThis();
        Signature signature = joinPoint.getSignature();
        Object[] args = joinPoint.getArgs();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method targetMethod = methodSignature.getMethod();
        DistributeLock distributeLock = targetMethod.getAnnotation(DistributeLock.class);
        if (Objects.nonNull(distributeLock)) {
            String lockPath = getLockPath(targetMethod, args, target, distributeLock.lockPath());
            long timeout = distributeLock.timeout();
            TimeUnit timeUnit = distributeLock.timeUnit();
            long leaseTime = distributeLock.leaseTime();
            RLock lock = getRedisDistributeLock(lockPath);
            if (timeout < 0) {
                lock.lock(leaseTime, timeUnit);
                log.info("获取到redis分布式锁：{}，leaseTime：{}，timeUnit：{}", lockPath, leaseTime, timeUnit.toString());
                try {
                    return joinPoint.proceed();
                } finally {
                    if (lock.isHeldByCurrentThread()) {
                        lock.unlock();
                        log.info("redis分布式锁：{}，leaseTime：{}，timeUnit：{}释放成功", lockPath, leaseTime, timeUnit);
                    } else {
                        log.info("获取redis分布式锁：{}，leaseTime：{}，timeUnit：{}不能被当前线程：{}释放", lockPath, leaseTime, timeUnit, Thread.currentThread().getName() + Thread.currentThread().getId());
                    }
                }
            } else {
                boolean locked = lock.tryLock(timeout, leaseTime, timeUnit);
                if (locked) {
                    log.info("获取到redis分布式锁：{}，timeout：{}，leaseTime：{}，timeUnit：{}", lockPath, timeout, leaseTime, timeUnit.toString());
                    try {
                        return joinPoint.proceed();
                    } finally {
                        if (lock.isHeldByCurrentThread()) {
                            lock.unlock();
                            log.info("redis分布式锁：{}，timeout：{}，leaseTime：{}，timeUnit：{}释放成功", lockPath, timeout, leaseTime, timeUnit);
                        } else {
                            log.info("获取redis分布式锁：{}，timeout：{}，leaseTime：{}，timeUnit：{}不能被当前线程：{}释放", lockPath, timeout, leaseTime, timeUnit, Thread.currentThread().getName() + Thread.currentThread().getId());
                        }
                    }
                } else {
                    log.warn("redis分布式锁：{}未获取到", lockPath);
                    String notGetLockMethodName = distributeLock.notGetLockMethod();
                    Method notGetLockMethod = target.getClass().getMethod(notGetLockMethodName, methodSignature.getParameterTypes());
                    return notGetLockMethod.invoke(target, args);
                }
            }

        }
        return joinPoint.proceed();
    }

    private RLock getRedisDistributeLock(String lockPath) {
        if (Objects.isNull(redissonClient)) {
            throw new RuntimeException("分布式锁工具类无法注入，请检查配置项monster.redisson.*是否正确配置");
        }
        return redissonClient.getLock(lockPath);
    }

    private String getLockPath(Method targetMethod, Object[] args, Object target, String lockPathExpression) {
        Map<String, Object> variables = loadArguments(targetMethod, target, args);
        String[] expressionArray = lockPathExpression.split("\\+");
        StringBuilder builder = new StringBuilder();
        for (String expression : expressionArray) {
            if (expression.startsWith("#")) {
                builder.append("#{");
                builder.append(expression);
                builder.append("}");
            } else {
                builder.append(expression);
            }
        }
        return CustomExpressionParser.getValueByExpression(variables, builder.toString()).toString();
    }


    private Map<String, Object> loadArguments(Method targetMethod, Object target, Object[] arguments) {
        Map<String, Object> variables = new ConcurrentHashMap<>(16);
        LockExpressionRoot root = new LockExpressionRoot(targetMethod, arguments, target, AopProxyUtils.ultimateTargetClass(target));
        variables.put("root", root);
        if (ObjectUtils.isEmpty(arguments)) {
            return variables;
        }
        ParameterNameDiscoverer parameterNameDiscoverer = new StandardReflectionParameterNameDiscoverer();
        String[] paramNames = parameterNameDiscoverer.getParameterNames(targetMethod);
        int paramCount = (paramNames != null ? paramNames.length : targetMethod.getParameterCount());
        int argsCount = arguments.length;
        for (int i = 0; i < paramCount; i++) {
            Object value = null;
            if (argsCount > paramCount && i == paramCount - 1) {
                value = Arrays.copyOfRange(arguments, i, argsCount);
            } else if (argsCount > i) {
                value = arguments[i];
            }
            value = value == null ? "" : value;
            variables.put("p" + i, value);
            variables.put("a" + i, value);
            if (paramNames != null && paramNames[i] != null) {
                variables.put(paramNames[i], value);
            }
        }
        return variables;
    }
}
